#!/usr/bin/python

import sys
import os
import random
import traceback
import optparse

import serial

import itrsrpc.config
import itrsrpc.util
from itrsrpc.util import (main_function, main_multicall_dispatch,
        round_up_to_block_size)
from itrsrpc import util, rpc

DEBUG = 0

hardware_settings = None

def print_memory(mem, default_value=0):
    count = 0
    for (i, v) in enumerate(mem):
        if v != default_value:
            sys.stdout.write('%02x:%02x ' % (i, v))
            count += 1
            if count % 10 == 0:
                sys.stdout.write('\n')

def random_bytes(size=256):
    return [ random.randrange(256) for i in xrange(size) ]

def test_escape(d):
    for i in xrange(20):
        d.echo([random.choice((ord('\n'), ord('\\')))])

def test_payload_values(d, verbose=0):
    for i in xrange(256):
        if verbose:
            sys.stderr.write('\rv: %03d' % (i))
        d.echo([i])
    if verbose:
        sys.stderr.write('\n')

def test_echo(d, max_length=1, n=100, verbose=0):
    def gen_message():
        while 1:
            yield random_bytes(max_length)
            if random.randrange(2) == 0:
                l = random.randrange(max_length+1)
                yield random_bytes(l)
    i = 0
    for (i, m) in enumerate(util.take(n, gen_message())):
        d.echo(m)
        if verbose and i % 10 == 0:
            sys.stderr.write('\rm: %d/%d' % (i, n))
    if verbose:
        if not i:
            return
        sys.stderr.write('\rm: %d/%d\n' % (i, n))

class ResetError(Exception):
    pass
def serial_rpc_reset(s):
    s.flushOutput()
    s.flushInput()
    t = s.getTimeout()
    s.setTimeout(.1)
    for i in xrange(20):
        rpc.message_write(s, [])
        try:
            if rpc.message_read(s) == []:
                break
        except rpc.MessageReadError:
            if DEBUG:
                sys.stderr.write('%s: ignoring exception:\n' %
                                 util.caller_name())
                traceback.print_exc(file=sys.stderr)
            s.flushInput()
    else:
        raise ResetError()
    s.setTimeout(t)

# handles error recovery
class ExceptionWrapper(object):
    def __init__(self, o, exc_handler):
        self.o = o
        self.exc_handler = exc_handler
    def __getattr__(self, attr):
        def helper(*args, **kw):
            try:
                return a(*args, **kw)
            except rpc.RPCError:
                self.exc_handler()
        a = getattr(self.o, attr)
        if callable(a):
            return helper
        else:
            return a

def test_transport(device, verbose=0):
    l = device.message_max()
    test_escape(device)
    test_payload_values(device)
    #FIXME: reserve 1 byte for METHOD_NUM, can't explain the necessary 2nd byte
    test_echo(device, max_length=l-2, n=1000, verbose=verbose)

def rpc_exc_handler():
    print >> sys.stderr, 'rpc_exc_handler:\n'
    traceback.print_exc(file=sys.stderr)
    print >> sys.stderr, 'rpc_exc_handler: resetting serial interface'
    serial_rpc_reset(s)
    reload(rpc)

def debug_connection_start(debug=0):
    s = serial.Serial(port=hardware_settings.SERIAL_DEVICE,
                    baudrate=hardware_settings.BAUDRATE,
                    bytesize=8, parity=serial.PARITY_NONE, stopbits=1,
                    rtscts=0, xonxoff=0,
                    timeout=10)
    d = rpc.Connection(s, debug=debug)
    return (s, d)

def hardware_settings_load():
    #FIXME: migrate away from hardware_settings to
    # runtime hardware identification and config files
    global hardware_settings
    try:
        hardware_settings = itrsrpc.config.load()
    except itrsrpc.config.ConfigMissing:
        sys.stderr.write('%s: config file missing. create one in %s or %s\n' %
                         (util.program_name(),
                          itrsrpc.config.system_config_path(),
                          itrsrpc.config.user_config_path()))
        t = itrsrpc.config.Config()
        d = dict(SERIAL_DEVICE='/dev/ttyUSB0', BAUDRATE=19200)
        t.__dict__ = d
        hardware_settings = t

def common_init(args):
    op = optparse.OptionParser(option_list=[
        optparse.Option('--verbose', action='store_true', default=True),
        optparse.Option('--debug', action='store_true', default=False,
                       help='enable debugging messages, '
                        'primarily for protocol debugging'),
        optparse.Option('--start-terminal-after-write', action='store_true',
                       help='start a serial terminal after programming '
                        'the device'),
    ])
    (options, args) = op.parse_args(args)
    hardware_settings_load()
    (s, d) = debug_connection_start(options.debug)
    serial_rpc_reset(s)
    return (s, d, options)

def serial_terminal(s):
    tty_file = file('/dev/tty', 'w+')
    util.terminal_input_set(tty_file)
    util.copy_files_forever([(tty_file, s), (s, tty_file)])

@main_function
def itrs_rpc_serial_terminal(args):
    hardware_settings_load()
    s = serial.Serial(port=hardware_settings.SERIAL_DEVICE,
                    baudrate=hardware_settings.BAUDRATE,
                    bytesize=8, parity=serial.PARITY_NONE, stopbits=1,
                    rtscts=hardware_settings.RTSCTS, xonxoff=0,
                    timeout=10)
    serial_terminal(s)

if __name__ == '__main__':
    # workaround ipython sys.argv behavior
    if util.program_name() == 'ipython':
        args = sys.argv[2:]
    else:
        args = sys.argv[1:]

    if util.program_name() == 'control':
        hardware_settings_load()
        (s, d) = debug_connection_start()
        d = ExceptionWrapper(d, rpc_exc_handler)
        serial_rpc_reset(s)
    else:
        try:
            main_multicall_dispatch(args)
        except (ResetError, rpc.RPCError), e:
            traceback.print_exc()
            sys.stderr.write('%s: failed to communicate with device '
                             'using: %s: baudrate: %d\n' %
                                    (util.program_name(),
                                     hardware_settings.SERIAL_DEVICE,
                                     hardware_settings.BAUDRATE))
